/*
 * Copyright 2009 Yannick Stucki (yannickstucki.com)
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 * 
 * http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package com.yannickstucki.android.musicqueue;

import java.util.ArrayList;
import java.util.LinkedList;
import java.util.Random;
import java.util.TreeSet;

import android.content.Context;
import android.content.SharedPreferences;
import android.os.Handler;

/**
 * This class handles the logic behind the songs. It is storing all the song as
 * well as managing which songs are currently displayed in the list.
 * 
 * @author Yannick Stucki (yannickstucki@gmail.com)
 * 
 */
public class SongManager {

  /**
   * An adapter to notify data change.
   * 
   * TODO: maybe put this somewhere outside and inject a handler.
   */
  private SongAdapter adapter;

  /**
   * The index of the song that should be hidden on the list (-1 if all are
   * visible). Used to hide the song under the hovering song.
   */
  public int hideSongIndex = -1;

  /**
   * The number of songs in the main view.
   * 
   * TODO: maybe make this dynamic depending on screen size?
   */
  public static final int COUNT = 5;

  /**
   * An array holding all the songs.
   */
  public Song[] songs = new Song[COUNT];

  /**
   * A list of the enqueued songs. Songs in the queue are those that are not
   * displayed, but will be added to the playing list next. If the queue is
   * empty, random songs are chosen instead.
   */
  public LinkedList<Song> waitingQueue = new LinkedList<Song>();

  /**
   * A list of all songs.
   */
  private ArrayList<Song> allSongs = new ArrayList<Song>();

  /**
   * A set storing all the songs that are starred.
   */
  private TreeSet<Song> starredSongs = new TreeSet<Song>();

  /**
   * A random number generator for picking songs if the queue is empty.
   */
  private Random random = new Random();

  /**
   * The index of the item where the finger is currently hovering. When the
   * finger goes up, that's the index where the to be moved item has to be
   * placed.
   */
  private int lastMoveIndex = -1;

  /**
   * The song which is being moved.
   */
  private Song moveSong;

  /**
   * A reference to the player service.
   * 
   * TODO: separation.
   */
  private PlayerService playerService;

  /**
   * The database is currently used for starred songs.
   */
  private DbAdapter database;

  /**
   * A reference to the songUpdater which will update the song list in its own
   * thread.
   */
  private SongUpdater songUpdater;

  /**
   * Constructor.
   */
  public SongManager(PlayerService playerService, DbAdapter database) {
    this.playerService = playerService;
    random.setSeed(System.currentTimeMillis());
    this.database = database;
  }

  /**
   * Initialization is called shortly after the constructor. It's separate
   * because of the return value.
   */
  public void init(final Context context, final Handler handler,
      final Runnable updatedResults) {
    database.open();

    SongUpdatedHandler songUpdatedHandler = new SongUpdatedHandler() {

      @Override
      public void onSongsUpdated() {
        allSongs = songUpdater.getSongs();
        if (allSongs.isEmpty()) { // no songs found... abort everything, the
          // player is not functional this way
          return;
        }

        // Try to restore the state from before: load the current song list +
        // the waiting queue
        SharedPreferences settings = context.getSharedPreferences("savedState",
            0);
        int numberOfSongs = allSongs.size();
        if (settings.contains("nWaiting")) { // we have a saved state!
          for (int i = 0; i < COUNT; i++) {
            int index = settings.getInt("songs" + i, 0);
            if (index >= numberOfSongs) {
              index = random.nextInt(numberOfSongs);
            }
            songs[i] = allSongs.get(index);
          }
          int nWaiting = settings.getInt("nWaiting", 0);
          for (int i = 0; i < nWaiting; i++) { // the waiting queue
            int index = settings.getInt("waiting" + i, 0);
            if (index >= numberOfSongs) {
              index = random.nextInt(numberOfSongs);
            }
            waitingQueue.addLast(allSongs.get(index));
          }
        } else {
          for (int i = 0; i < COUNT; i++) {
            songs[i] = getSong();
          }
        }

        SharedPreferences.Editor editor = settings.edit();
        editor.clear();
        editor.commit();

	// TODO: do this at a higher level ?
        if (! database.isOpen()) database.open();
	
        Song[] starred = database.fetchAllSongs();

        // The songs from the database don't contain all the information so we
        // put
        // this songs into this temporary set.
        TreeSet<Song> starredSongsTemp = new TreeSet<Song>();

        for (int i = 0; i < starred.length; i++) {
          starredSongsTemp.add(starred[i]);
        }

        if (starredSongsTemp.size() > 0) {
          for (int i = 0; i < numberOfSongs; i++) {
            if (starredSongsTemp.contains(allSongs.get(i))) {
              allSongs.get(i).starred = true;
              // Add the songs to the "real" starred songs
              starredSongs.add(allSongs.get(i));
            }
          }
        }
        handler.post(updatedResults);
      }
    };
    updateSongList(context, songUpdatedHandler); // load the songs
  }

  /**
   * Sets the adapter reference.
   */
  public void setAdapter(SongAdapter adapter) {
    this.adapter = adapter;
  }

  /**
   * Removes an item from the list.
   * 
   * TODO: doesn't work nicely if remove is called while the user is still
   * dragging a song.
   * 
   * @param position
   *          the index of the item to be removed
   */
  public void remove(int position) {
    if (position < 0 || position >= COUNT) {
      Logger.out("ERROR POSITION OUT OF BOUNDS: " + position);
      return;
    }

    for (int i = position; i < COUNT - 1; i++) {
      songs[i] = songs[i + 1];
    }

    songs[COUNT - 1] = getSong();
    adapter.notifyDataSetChanged();
    if (position == 0) {
      playerService.listUpdated();
    }
  }

  /**
   * Called when the user puts his finger on the screen.
   * 
   * @param position
   *          the index of the item where the user put his finger.
   */
  public void down(int position) {
    if (position < 0 || position >= COUNT) {
      return;
    }
    lastMoveIndex = position;
    moveSong = songs[position];
    hideSongIndex = position;
    adapter.notifyDataSetChanged();
  }

  /**
   * Called when the user moves his finger.
   * 
   * @param position
   *          the index in the list of the songs, with 0 being the topmost song.
   */
  public void move(int position) { // user moves his finger
    if (position == lastMoveIndex) {
      return;
    }
    if (position < lastMoveIndex) { // moved up
      for (int i = lastMoveIndex; i > position; i--) {
        songs[i] = songs[i - 1];
      }
    } else { // moved down
      for (int i = lastMoveIndex; i < position; i++) {
        songs[i] = songs[i + 1];
      }
    }
    songs[position] = moveSong;
    hideSongIndex = position;
    adapter.notifyDataSetChanged();
    lastMoveIndex = position;
  }

  /**
   * Called when a user takes his finger off the screen. Performs the actual
   * move of the item.
   */
  public void up() {
    hideSongIndex = -1;
    // Let the player service know about the change so if the song got dropped
    // on top it can be played now.
    playerService.listUpdated();
    adapter.notifyDataSetChanged();
  }

  /**
   * Replaces all the songs in the list (except the one currently playing) by a
   * new one.
   */
  public void shuffle() {
    for (int i = 1; i < COUNT; i++) {
      songs[i] = getSong();
    }
    adapter.notifyDataSetChanged();
  }

  /**
   * Returns a new song for filling the list. If the queue is non empty, the
   * song is taken from there, else it's taken randomly.
   */
  private Song getSong() {
    int waitingQueueSize = waitingQueue.size();
    if (waitingQueueSize > 0) {
      playerService.updateQueueSizeButton(waitingQueueSize - 1);
      return waitingQueue.removeFirst();
    } else {
      return allSongs.get(random.nextInt(allSongs.size()));
    }
  }

  /**
   * This method is called once on start up. It fetches all the songs from the
   * {@link MediaStore}.
   * 
   * @param context
   *          needed to extract the songs
   */
  public void updateSongList(Context context, SongUpdatedHandler handler) {
    songUpdater = new SongUpdater(context, handler);
    Thread t = new Thread(songUpdater);
    t.start();
  }

  /**
   * Adds a starred song to the set and the database.
   */
  public void addStarredSong(Song song) {
	// TODO: do this at a higher level ?
	if (! database.isOpen()) database.open();    
	if (starredSongs.add(song)) database.addSong(song);
  }

  /**
   * Removes a starred song from the set and the database.
   */
  public void removeStarredSong(Song song) {
	// TODO: do this at a higher level ?
	if (! database.isOpen()) database.open();    
	if (starredSongs.remove(song)) database.deleteSong(song);
  }

  /**
   * Returns an array of all the starred songs.
   */
  public Song[] getStarredSongs() {
    return starredSongs.toArray(new Song[0]);
  }

  /**
   * Returns an array of all the songs.
   */
  public Song[] getAllSongs() {
    return allSongs.toArray(new Song[0]);
  }

  /**
   * Closes the connection to the database. Call when song manager isn't needed
   * anymore.
   */
  public void close() {
	// TODO: check database before trying to close it ?
	database.close();
  }

  /**
   * Returns whether the song manager is empty or not.
   */
  public boolean isEmpty() {
    return allSongs.isEmpty();
  }
}
